package se.webbzon.boltzmann.object.background;

import java.awt.Color;
import java.awt.Dimension;
import java.awt.Image;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;

import se.webbzon.boltzmann.camera.AbstractCamera;
import se.webbzon.boltzmann.camera.Camera;
import se.webbzon.boltzmann.camera.CameraListener;
import se.webbzon.boltzmann.collector.Collector;
import se.webbzon.boltzmann.collector.CollectorListener;
import se.webbzon.boltzmann.debug.Debug;
import se.webbzon.boltzmann.environment.AbstractEnvironment;
import se.webbzon.boltzmann.resource.Resources;
import se.webbzon.oschi01.resourceset.ResourceSet;
import se.webbzon.oschi01.worldarea.WorldArea;
import se.webbzon.oschi01.worldcamera.WorldCameraPane;
import se.webbzon.oschi01.worldcamera.WorldView;
import se.webbzon.oschi01.worldeditor.DeflatableWorldObject;
import se.webbzon.oschi01.worldeditor.WorldObjectDeflation;
import se.webbzon.oschi01.worldobject.WorldObject;

public class Background extends WorldObject implements DeflatableWorldObject {
	
	// The background translation coefficients
	private static final double cRY = 1.5, cRX = 4;
	
	// The name of the deflatable world object
	public static final String worldObjectName = "background";
	
	// The 2-dimensional depth of this background
	private static final int depth = 1000;
	
	// A map containing all the abstract cameras
	private final Map<WorldCameraPane,AbstractCamera> cameraMap;
	
	// A listener to the cameras
	private final CameraListener listener;
	
	// The name of the background image
	private String backgroundName;
	
	// The background image resource set
	private ResourceSet<Image> background;
	
	// The width and height of the background along with the
	// maximal amplitude of the panning and tilt of the 
	// background.
	private double width, height, panningMax, tiltMax;
	
	// The background color if no background image is defined
	private Color color;
	
	// A collector of all camera instances
	private volatile Collector<AbstractCamera> cameraCollector;
	
	/*============================================================
	Constructors
	============================================================*/
	
	/** Creates a new black background. **/
	public Background(AbstractEnvironment env) {
		this(env,null);
	}
		
	/** Creates a new background by loading the background image
	 * resource set with the given name. **/
	public Background(AbstractEnvironment env, String backgroundName) {
		this(env,backgroundName,Color.black,2,1.25,0.5,0.25);
	}

	/** Creates a new background by loading the background image
	 * resource set with the given name. A maximal horizontal and
	 * vertical offset of the background may also be provided
	 * in this constructor. **/
	public Background(	AbstractEnvironment env, 
						String backgroundName, Color color,
						double width, double height,
						double panningMax, double tiltMax) {
		super(	WorldObject.onCreate + WorldObject.onDestroy +
				WorldObject.onPreDraw, depth);
		this.width = width;
		this.height = height;
		this.panningMax = panningMax;
		this.tiltMax = tiltMax;
		this.color = color;
		cameraMap = new HashMap<WorldCameraPane,AbstractCamera>();
		listener = new CameraListenerImpl();
		
		setBackground(backgroundName);
	}
	
	/*============================================================
	Public Methods
	============================================================*/
	
	/** Returns the color of the background if no background image
	 * is defined. **/
	public Color getColor() {
		return color;
	}
	
	/** Sets the color of the background if no background image
	 * is defined. **/
	public void setColor(Color color) {
		this.color = color;
	}
	
	/** Returns the width of the background. The default value
	 * is 2.0. **/
	public double getWidth() {
		return width;
	}
	
	/** Sets the width of the background. The background width is 
	 * defined as a scalar with a default value of 2.0. **/
	public void setWidth(double width) {
		this.width = width;
	}
	
	/** Returns the height of the background. The default value
	 * is 2.0. **/
	public double getHeight() {
		return height;
	}
	
	/** Sets the height of the background. The background height is 
	 * defined as a scalar with a default value of 2.0. **/
	public void setHeight(double height) {
		this.height = height;
	}
	
	/** Returns the maximal panning of the background
	 * when an input is applied. The default value is 0.5. **/
	public double getPanningMax() {
		return panningMax;
	}
	
	/** Sets the maximal panning of the background when an 
	 * input is applied. The panning is defined as a scalar
	 * and is equal to 0.5 by default which makes most of the 
	 * background visible. **/
	public void setPanningMax(double panningMax) {
		this.panningMax = panningMax;
	}
	
	/** Returns the maximal tilt of the background
	 * when an input is applied. The default value is 0.5. **/
	public double getTiltMax() {
		return tiltMax;
	}
	
	/** Sets the maximal tilt of the background
	 * when an input is applied. The tilt is defined as a scalar
	 * and is equal to 0.5 by default which makes most of the 
	 * background visible. **/
	public void setTiltMax(double tiltMax) {
		this.tiltMax = tiltMax;
	}
	
	/** Returns the name of the background. **/
	public String getBackgroundName() {
		return backgroundName;
	}
	
	/** Returns the image resource set associated with this background. **/
	public ResourceSet<Image> getBackground() {
		return background;
	}
	
	/** Sets the background image by loading the image resource set 
	 * with the given name. If the background can not be found or if
	 * the given name is null then a black background will be used. **/
	public void setBackground(String backgroundName) {
		final ResourceSet<Image> background;
		if (backgroundName == null || backgroundName.isEmpty()) {
			// Load no background
			background = null;
		} else {
			// Loads the background with the given name
			background = Resources.getImageSet(backgroundName);
		}
		
		if (background == null) {
			this.backgroundName = "";
			this.background = null;
		} else {
			this.backgroundName = backgroundName;
			this.background = background;
		}
	}
	
	@Override public void onPreDraw(WorldArea world, WorldCameraPane background) {
		Dimension d = new Dimension();
		background.getLayer().getDimension(d);
		if (this.background == null) {
			// Single colored background
			background.setColor(color);
			background.fillRect(0, 0, d.width, d.height);
			
		} else {
			Camera camera = cameraMap.get(background);
			if (camera == null) {
				// Draw alternative background if player environment is undefined
				final int x = - (int) (0.5 * d.getWidth() * (width - 1));
				final int y = - (int) (0.5 * d.getHeight() * (height - 1));
				background.drawImage(	this.background.get(0), x, y, 
										(int) (width * d.getWidth()), 
										(int) (height * d.getHeight()));
			} else {
				// Draw background
				double rY = camera.getRotationY();
				rY = rY > 0 ? rY - Math.PI : rY + Math.PI;
				double rX = camera.getRotationX();
				rX = rX + Math.PI/2;
				
				final double x = - (d.getWidth() * ((width - 1) * 0.5 - panningMax * cRY * rY));
				final double y = - (d.getHeight() * ((height - 1) * 0.5 - tiltMax * cRX * rX));
				final int w = (int) (width * d.getWidth());
				final int h = (int) (height * d.getHeight());
				Image bgImage = this.background.get(0);
				int xStart = x < 0 ? (int) x : (int) (x - w * Math.ceil(x/w));
				int yStart = y < 0 ? (int) y : (int) (y - h * Math.ceil(y/h));
				for (int xd = xStart; xd < d.width; xd+=w) {
					for (int yd = yStart; yd < d.height; yd+=h) {
						background.drawImage(bgImage, xd, yd, w, h);
					}
				}
			}
		}
	}
	
	@Override public void onCreate(WorldArea world) {
		cameraCollector = Collector.getCollector(world, AbstractCamera.class);
		cameraCollector.addListener(new CollectorListenerImpl());
		Collection<AbstractCamera> cameras = cameraCollector.getInstances();
		synchronized(cameras) {
			for (AbstractCamera camera : cameras) {
				camera.addListener(listener);
				Collection<WorldView> views = camera.getViews();
				synchronized (views) {
					for (WorldView view : views) {
						cameraMap.put(view.getCameraBackground(), camera);
					}
				}
			}
		}
	}
	
	@Override public void onDestroy(WorldArea world) {
		cameraMap.clear();
		cameraCollector = null;
	}

	@Override public String getName() {
		return worldObjectName;
	}

	@Override public WorldObjectDeflation getDeflation() {
		return new BackgroundDeflation(0,this);
	}
	
	/*============================================================
	Private Classes
	============================================================*/
	
	/** An abstract camera collector. **/
	private final class CollectorListenerImpl implements CollectorListener<AbstractCamera> {

		@Override public void onCreateInstance(	Collector<AbstractCamera> collector,
												AbstractCamera instance) {
			Collection<WorldView> views = instance.getViews();
			synchronized (views) {
				for (WorldView view : views) {
					cameraMap.put(view.getCameraBackground(), instance);
					int d;
					Debug.debugMap(this, cameraMap);
				}
			}
			instance.addListener(listener);
		}

		@Override public void onDestroyInstance(Collector<AbstractCamera> collector,
												AbstractCamera instance) {
			Collection<WorldView> views = instance.getViews();
			synchronized (views) {
				for (WorldView view : views) {
					cameraMap.remove(view.getCameraBackground());
				}
			}
			instance.removeListener(listener);
		}

		@Override public void onDestroyCollector(Collector<AbstractCamera> collector) {
			cameraMap.clear();
		}
		
	}
	
	/** An implementation of a camera listener. **/
	private final class CameraListenerImpl implements CameraListener {

		@Override public void onViewAdded(AbstractCamera camera, WorldView view) {
			cameraMap.put(view.getCameraBackground(), camera);
		}

		@Override public void onViewRemoved(Camera camera, WorldView view) {
			cameraMap.remove(view.getCameraBackground());
		}

		@Override public void onViewsCleared(Camera camera) {
			Collection<WorldView> views = camera.getViews();
			synchronized (views) {
				for (WorldView view : views) {
					cameraMap.remove(view.getCameraBackground());
				}
			}
		}
	}

}
